/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * HTML.java
 * Copyright (C) 2009-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.classifiers.evaluation.output.prediction;

import weka.classifiers.Classifier;
import weka.core.Instance;
import weka.core.Utils;

/**
 * <!-- globalinfo-start --> Outputs the predictions in HTML.
 * <p/>
 * <!-- globalinfo-end -->
 *
 * <!-- options-start --> Valid options are:
 * <p/>
 * 
 * <pre>
 *  -p &lt;range&gt;
 *  The range of attributes to print in addition to the classification.
 *  (default: none)
 * </pre>
 * 
 * <pre>
 *  -distribution
 *  Whether to turn on the output of the class distribution.
 *  Only for nominal class attributes.
 *  (default: off)
 * </pre>
 * 
 * <pre>
 *  -decimals &lt;num&gt;
 *  The number of digits after the decimal point.
 *  (default: 3)
 * </pre>
 * 
 * <pre>
 *  -file &lt;path&gt;
 *  The file to store the output in, instead of outputting it on stdout.
 *  Gets ignored if the supplied path is a directory.
 *  (default: .)
 * </pre>
 * 
 * <pre>
 *  -suppress
 *  In case the data gets stored in a file, then this flag can be used
 *  to suppress the regular output.
 *  (default: not suppressed)
 * </pre>
 * 
 * <!-- options-end -->
 *
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class HTML extends AbstractOutput {

    /** for serialization. */
    private static final long serialVersionUID = 7241252244954353300L;

    /**
     * Returns a string describing the output generator.
     * 
     * @return a description suitable for displaying in the GUI
     */
    public String globalInfo() {
        return "Outputs the predictions in HTML.";
    }

    /**
     * Returns a short display text, to be used in comboboxes.
     * 
     * @return a short display text
     */
    public String getDisplay() {
        return "HTML";
    }

    /**
     * Replaces certain characters with their HTML entities.
     * 
     * @param s the string to process
     * @return the processed string
     */
    protected String sanitize(String s) {
        String result;

        result = s;
        result = result.replaceAll("&", "&amp;");
        result = result.replaceAll("<", "&lt;");
        result = result.replaceAll(">", "&gt;");
        result = result.replaceAll("\"", "&quot;");

        return result;
    }

    /**
     * Performs the actual printing of the header.
     */
    protected void doPrintHeader() {
        append("<html>\n");
        append("<head>\n");
        append("<title>Predictions for dataset " + sanitize(m_Header.relationName()) + "</title>\n");
        append("</head>\n");
        append("<body>\n");
        append("<div align=\"center\">\n");
        append("<h3>Predictions for dataset " + sanitize(m_Header.relationName()) + "</h3>\n");
        append("<table border=\"1\">\n");
        append("<tr>\n");
        if (m_Header.classAttribute().isNominal())
            if (m_OutputDistribution)
                append("<td>inst#</td><td>actual</td><td>predicted</td><td>error</td><td colspan=\"" + m_Header.classAttribute().numValues() + "\">distribution</td>");
            else
                append("<td>inst#</td><td>actual</td><td>predicted</td><td>error</td><td>prediction</td>");
        else
            append("<td>inst#</td><td>actual</td><td>predicted</td><td>error</td>");

        if (m_Attributes != null) {
            append("<td>");
            boolean first = true;
            for (int i = 0; i < m_Header.numAttributes(); i++) {
                if (i == m_Header.classIndex())
                    continue;

                if (m_Attributes.isInRange(i)) {
                    if (!first)
                        append("</td><td>");
                    append(sanitize(m_Header.attribute(i).name()));
                    first = false;
                }
            }
            append("</td>");
        }

        append("</tr>\n");
    }

    /**
     * Builds a string listing the attribute values in a specified range of indices,
     * separated by commas and enclosed in brackets.
     *
     * @param instance the instance to print the values from
     * @return a string listing values of the attributes in the range
     */
    protected String attributeValuesString(Instance instance) {
        StringBuffer text = new StringBuffer();
        if (m_Attributes != null) {
            boolean firstOutput = true;
            m_Attributes.setUpper(instance.numAttributes() - 1);
            for (int i = 0; i < instance.numAttributes(); i++)
                if (m_Attributes.isInRange(i) && i != instance.classIndex()) {
                    if (!firstOutput)
                        text.append("</td>");
                    if (m_Header.attribute(i).isNumeric())
                        text.append("<td align=\"right\">");
                    else
                        text.append("<td>");
                    text.append(sanitize(instance.toString(i)));
                    firstOutput = false;
                }
            if (!firstOutput)
                text.append("</td>");
        }
        return text.toString();
    }

    protected void doPrintClassification(double[] dist, Instance inst, int index) throws Exception {
        int prec = m_NumDecimals;

        Instance withMissing = (Instance) inst.copy();
        withMissing.setDataset(inst.dataset());

        double predValue = 0;
        if (Utils.sum(dist) == 0) {
            predValue = Utils.missingValue();
        } else {
            if (inst.classAttribute().isNominal()) {
                predValue = Utils.maxIndex(dist);
            } else {
                predValue = dist[0];
            }
        }

        // index
        append("<tr>");
        append("<td>" + (index + 1) + "</td>");

        if (inst.dataset().classAttribute().isNumeric()) {
            // actual
            if (inst.classIsMissing())
                append("<td align=\"right\">" + "?" + "</td>");
            else
                append("<td align=\"right\">" + Utils.doubleToString(inst.classValue(), prec) + "</td>");
            // predicted
            if (Utils.isMissingValue(predValue))
                append("<td align=\"right\">" + "?" + "</td>");
            else
                append("<td align=\"right\">" + Utils.doubleToString(predValue, prec) + "</td>");
            // error
            if (Utils.isMissingValue(predValue) || inst.classIsMissing())
                append("<td align=\"right\">" + "?" + "</td>");
            else
                append("<td align=\"right\">" + Utils.doubleToString(predValue - inst.classValue(), prec) + "</td>");
        } else {
            // actual
            append("<td>" + ((int) inst.classValue() + 1) + ":" + sanitize(inst.toString(inst.classIndex())) + "</td>");
            // predicted
            if (Utils.isMissingValue(predValue))
                append("<td>" + "?" + "</td>");
            else
                append("<td>" + ((int) predValue + 1) + ":" + sanitize(inst.dataset().classAttribute().value((int) predValue)) + "</td>");
            // error?
            if (!Utils.isMissingValue(predValue) && !inst.classIsMissing() && ((int) predValue + 1 != (int) inst.classValue() + 1))
                append("<td>" + "+" + "</td>");
            else
                append("<td>" + "&nbsp;" + "</td>");
            // prediction/distribution
            if (m_OutputDistribution) {
                if (Utils.isMissingValue(predValue)) {
                    append("<td>" + "?" + "</td>");
                } else {
                    append("<td align=\"right\">");
                    for (int n = 0; n < dist.length; n++) {
                        if (n > 0)
                            append("</td><td align=\"right\">");
                        if (n == (int) predValue)
                            append("*");
                        append(Utils.doubleToString(dist[n], prec));
                    }
                    append("</td>");
                }
            } else {
                if (Utils.isMissingValue(predValue))
                    append("<td align=\"right\">" + "?" + "</td>");
                else
                    append("<td align=\"right\">" + Utils.doubleToString(dist[(int) predValue], prec) + "</td>");
            }
        }

        // attributes
        append(attributeValuesString(withMissing) + "</tr>\n");
    }

    /**
     * Store the prediction made by the classifier as a string.
     * 
     * @param classifier the classifier to use
     * @param inst       the instance to generate text from
     * @param index      the index in the dataset
     * @throws Exception if something goes wrong
     */
    protected void doPrintClassification(Classifier classifier, Instance inst, int index) throws Exception {

        double[] d = classifier.distributionForInstance(inst);
        doPrintClassification(d, inst, index);
    }

    /**
     * Does nothing.
     */
    protected void doPrintFooter() {
        append("</table>\n");
        append("</div>\n");
        append("</body>\n");
        append("</html>\n");
    }
}
